'use strict';

// color-board-pro.js — 色盘搜索升级版（支持直接试听 + 列表行样式匹配）
(function(){
  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

  function init(){
    var mount = document.getElementById('color-board');
    var resultsBox = document.getElementById('color-results');
    var section = document.getElementById('color-search');
    if (!mount || !resultsBox || !section) return;

    // 清空原有简版 UI（若存在）
    mount.innerHTML = '';
    resultsBox.innerHTML = '';

    // 共享音频播放器（单实例）
    var audio = new Audio();
    audio.preload = 'none';
    var currentPlayId = null; // 当前播放的 track id

    // 工具函数：颜色 & 文本 & 数组
    function clamp(v, a, b){ return Math.min(b, Math.max(a, v)); }
    function hex(n){ return n.toString(16).padStart(2,'0'); }
    function rgbToHex(r,g,b){ return '#' + hex(r) + hex(g) + hex(b); }
    function hexToRgb(h){
      var m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h);
      return m ? { r:parseInt(m[1],16), g:parseInt(m[2],16), b:parseInt(m[3],16)} : null;
    }
    function hslToRgb(h, s, l){
      h = (h%360+360)%360; s/=100; l/=100;
      var c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs((h/60)%2-1)), m=l-c/2;
      var r=0,g=0,b=0;
      if(h<60){r=c;g=x;} else if(h<120){r=x;g=c;} else if(h<180){g=c;b=x;}
      else if(h<240){g=x;b=c;} else if(h<300){r=x;b=c;} else {r=c;b=x;}
      r=Math.round((r+m)*255); g=Math.round((g+m)*255); b=Math.round((b+m)*255);
      return {r:r,g:g,b:b};
    }
    function hexToHsl(H){
      var r=0,g=0,b=0;
      if (H.length==4){
        r="0x"+H[1]+H[1]; g="0x"+H[2]+H[2]; b="0x"+H[3]+H[3];
      } else {
        r="0x"+H[1]+H[2]; g="0x"+H[3]+H[4]; b="0x"+H[5]+H[6];
      }
      r/=255; g/=255; b/=255;
      var cmin=Math.min(r,g,b), cmax=Math.max(r,g,b), delta=cmax-cmin;
      var h=0, s=0, l=0;
      if (delta==0) h=0;
      else if (cmax==r) h=((g-b)/delta)%6;
      else if (cmax==g) h=(b-r)/delta+2;
      else h=(r-g)/delta+4;
      h=Math.round(h*60); if (h<0) h+=360;
      l=(cmax+cmin)/2;
      s=delta==0 ? 0 : delta/(1-Math.abs(2*l-1));
      s=Math.round(s*100); l=Math.round(l*100);
      return {h:h,s:s,l:l};
    }
    // Lab & ΔE2000
    function hexToLab(hex){
      var rgb = hexToRgb(hex); if(!rgb) return {l:0,a:0,b:0};
      function lin(v){ v/=255; return v<=0.04045? v/12.92 : Math.pow((v+0.055)/1.055,2.4); }
      var r=lin(rgb.r), g=lin(rgb.g), b=lin(rgb.b);
      var x=(r*0.4124564 + g*0.3575761 + b*0.1804375)/0.95047;
      var y=(r*0.2126729 + g*0.7151522 + b*0.0721750)/1.00000;
      var z=(r*0.0193339 + g*0.1191920 + b*0.9503041)/1.08883;
      function f(t){ return t>0.008856? Math.pow(t,1/3) : (7.787*t+16/116); }
      var fx=f(x), fy=f(y), fz=f(z);
      return { l:116*fy-16, a:500*(fx-fy), b:200*(fy-fz) };
    }
    function deltaE2000(Lab1){
      return function(Lab2){
        var L1=Lab1.l, a1=Lab1.a, b1=Lab1.b;
        var L2=Lab2.l, a2=Lab2.a, b2=Lab2.b;
        var avgL=(L1+L2)/2;
        var C1=Math.sqrt(a1*a1+b1*b1);
        var C2=Math.sqrt(a2*a2+b2*b2);
        var avgC=(C1+C2)/2;
        var G=0.5*(1-Math.sqrt(Math.pow(avgC,7)/(Math.pow(avgC,7)+Math.pow(25,7))));
        var a1p=(1+G)*a1, a2p=(1+G)*a2;
        var C1p=Math.sqrt(a1p*a1p+b1*b1);
        var C2p=Math.sqrt(a2p*a2p+b2*b2);
        function hp(a,b){ if(a===0 && b===0) return 0; var ang=Math.atan2(b,a)*180/Math.PI; return ang<0?ang+360:ang; }
        var h1p=hp(a1p,b1), h2p=hp(a2p,b2);
        var dLp=L2-L1, dCp=C2p-C1p;
        var dh=h2p-h1p; if (dh>180) dh-=360; else if (dh<-180) dh+=360;
        var dHp=2*Math.sqrt(C1p*C2p)*Math.sin((dh*Math.PI/180)/2);
        function avgH(h1,h2){ if (C1p*C2p===0) return h1+h2; var diff=Math.abs(h1-h2); if (diff<=180) return (h1+h2)/2; return (h1+h2+360)/2; }
        var avgHp=avgH(h1p,h2p);
        var T=1-0.17*Math.cos((avgHp-30)*Math.PI/180)+0.24*Math.cos((2*avgHp)*Math.PI/180)+0.32*Math.cos(((3*avgHp)+6)*Math.PI/180)-0.20*Math.cos(((4*avgHp)-63)*Math.PI/180);
        var dRo=30*Math.exp(-Math.pow((avgHp-275)/25,2));
        var Rc=2*Math.sqrt(Math.pow(avgC,7)/(Math.pow(avgC,7)+Math.pow(25,7)));
        var Sl=1+(0.015*Math.pow((avgL-50),2))/Math.sqrt(20+Math.pow((avgL-50),2));
        var Sc=1+0.045*avgC;
        var Sh=1+0.015*avgC*T;
        var Rt=-Math.sin(2*(dRo*Math.PI/180))*Rc;
        var Kl=1,Kc=1,Kh=1;
        return Math.sqrt(Math.pow(dLp/(Kl*Sl),2)+Math.pow(dCp/(Kc*Sc),2)+Math.pow(dHp/(Kh*Sh),2)+Rt*(dCp/(Kc*Sc))*(dHp/(Kh*Sh)));
      }
    }

    // 状态
    var state = {
      colors: [],            // 多选
      tol: 12,               // ΔE 容差
      harmony: null,         // 'analogous' | 'triad' | 'complement' | null
      presets: loadJSON('CBX_PRESETS', []),
      recent: loadJSON('CBX_RECENT', []),
      pageSize: 40
    };

    // DEMO（仅在无后端时用于展示）
    var DEMO = [
      { id:'d1', title:'霓虹疾走', artist:'Ruotong Studio', color:'#6EF3FF', bpm:128, duration:124, tags:['电子','合成器','鼓机'], preview:'assets/demo.wav' },
      { id:'d2', title:'晨雾疗愈', artist:'Ruotong Studio', color:'#8BFF96', bpm:90,  duration:192, tags:['原声','钢琴','弦乐'], preview:'assets/demo.wav' },
      { id:'d3', title:'空轨悬疑', artist:'Ruotong Studio', color:'#2A6CFB', bpm:110, duration:160, tags:['氛围','低频','脉冲'], preview:'assets/demo.wav' },
      { id:'d4', title:'春日和风', artist:'Ruotong Studio', color:'#FFC6A7', bpm:122, duration:88,  tags:['和风','木鱼','笛子'], preview:'assets/demo.wav' }
    ];

    // 读 URL
    try {
      var u = new url(location.href);
      var qs = u.searchParams;
      var list = (qs.get('colors')||qs.get('color')||'').split(/[,\s]+/).filter(Boolean).map(normalizeHex);
      if (list.length) state.colors = uniq(list);
      var tol = parseInt(qs.get('tol'), 10); if (!Number.isNaN(tol)) state.tol = clamp(tol, 2, 40);
      var logic = (qs.get('logic')||'').toLowerCase(); if (['any','all','soft'].indexOf(logic)>=0) state.logic = logic;
      var rank = (qs.get('rank')||'').toLowerCase(); if (['weighted','min','avg','max'].indexOf(rank)>=0) state.rank = rank;
      var lp = parseFloat(qs.get('lp')); if (!Number.isNaN(lp)) state.lpen = Math.max(0, Math.min(1, lp));
      var harm = (qs.get('harmony')||'').toLowerCase(); state.harmony = ['analogous','triad','complement'].includes(harm)?harm:null;
    } catch(e){}

    // 渲染 UI
    section.insertAdjacentHTML('afterbegin', '<a class="skip-link" href="#color-results">跳到搜索结果</a>');
    mount.innerHTML = [
      '<div class="cbx">',
      '  <div class="cbx-left">',
      '    <div class="panel">',
      '      <h3>选择颜色</h3>',
      '      <div class="row">',
      '        <input type="color" id="cbx-color" value="'+(state.colors[0]||"#6EF3FF")+'"/>',
      '        <input type="text" class="hex" id="cbx-hex" value="'+(state.colors[0]||"#6EF3FF")+'" placeholder="#AABBCC"/>',
      '        <button type="button" id="cbx-add" class="pill">确认</button>',
      '        <button type="button" id="cbx-dropper" class="pill">吸管</button>
        <span class="muted" style="margin-left:8px;font-size:12px">选色后点击“确认”</span>',
      '      </div>',
      '      <div class="swatches" id="cbx-swatches"></div>',
      '      <div class="tip">可多选颜色；支持粘贴 HEX、取色器与色块点击。</div>',
      '    </div>',
      '    <div class="panel">',
      '      <h3>高级</h3>',
      '      <div class="row" style="gap:16px">',
      '        <label>容差 ΔE',
      '          <input type="range" id="cbx-tol" min="2" max="40" step="1" value="'+state.tol+'"/>',
      '          <span id="cbx-tol-val" class="kbd">'+state.tol+'</span>',
      '        </label>',
      '        <div class="actions" role="group" aria-label="和声">',
      '          <button type="button" class="harm" data-k="analogous" title="相邻色">相邻</button>',
      '          <button type="button" class="harm" data-k="triad" title="三角色">三角</button>',
      '          <button type="button" class="harm" data-k="complement" title="互补色">互补</button>',
      '        </div>',
      '      </div>',
      '      <div class="row" style="gap:16px">',
      '        <label>匹配逻辑',
      '          <select id="cbx-logic">',
      '            <option value="any"'+" + (state.logic==='any'?' selected':'') + ">任一</option>",
      '            <option value="all"'+" + (state.logic==='all'?' selected':'') + ">全部</option>",
      '            <option value="soft"'+" + (state.logic==='soft'?' selected':'') + ">软交集</option>",
      '          </select>',
      '        </label>',
      '        <label>排序策略',
      '          <select id="cbx-rank">',
      '            <option value="weighted"'+" + (state.rank==='weighted'?' selected':'') + ">加权</option>",
      '            <option value="min"'+" + (state.rank==='min'?' selected':'') + ">最小ΔE</option>",
      '            <option value="avg"'+" + (state.rank==='avg'?' selected':'') + ">平均ΔE</option>",
      '            <option value="max"'+" + (state.rank==='max'?' selected':'') + ">最大ΔE</option>",
      '          </select>',
      '        </label>',
      '        <label title="对明度差异的惩罚，适度提高可减少偏灰/过亮/过暗误匹配">亮度权重 L',
      '          <input type="range" id="cbx-lp" min="0" max="1" step="0.1" value="'+state.lpen+'"/>',
      '          <span id="cbx-lp-val" class="kbd">'+state.lpen+'</span>',
      '        </label>',
      '      </div>',
      '      <div class="chips" id="cbx-selected"></div>',
      '      <div class="actions">',
      '        <button type="button" id="cbx-save">存为预设</button>',
      '        <button type="button" id="cbx-clear">清空</button>',
      '      </div>',
      '      <div class="chips" id="cbx-presets"></div>',
      '    </div>',
      '    <div class="panel">',
      '      <h3>情绪捷径</h3>',
      '      <div class="chips" id="cbx-moods"></div>',
      '    </div>',
      '  </div>',
      '  <div>',
      '    <div class="toolbar">',
      '      <div class="summary" id="cbx-summary"></div>',
      '      <div class="actions"><span class="kbd">⌘/Ctrl</span> + <span class="kbd">K</span> 聚焦 HEX 输入</div>',
      '    </div>',
      '    <div class="list" id="cbx-list"></div>',
      '    <div id="cbx-sticky">',
      '      <div class="chips" id="cbx-sticky-chips"></div>',
      '      <div class="btns">',
      '        <button type="button" id="cbx-sticky-clear">清空</button>',
      '        <button type="button" id="cbx-sticky-backtotop">回到顶部</button>',
      '      </div>',
      '    </div>',
      '  </div>',
      '</div>'
    ].join('');

    // 色板与情绪映射
    var basePalette = buildBasePalette(7, 12, true);
    var moodMap = [
      { name:'科技 · 未来', hex:'#6EF3FF' },
      { name:'自然 · 轻快', hex:'#8BFF96' },
      { name:'温暖 · 活力', hex:'#FFD15F' },
      { name:'热烈 · 冲突', hex:'#FF7D7D' },
      { name:'甜美 · 浪漫', hex:'#FF8BD7' },
      { name:'治愈 · 宁静', hex:'#9BA4FF' },
      { name:'极简 · 明快', hex:'#E9ECF1' },
      { name:'深邃 · 神秘', hex:'#0B0D12' }
    ];

    // 构建 UI：色块、情绪、预设
    var swBox = document.getElementById('cbx-swatches');
    swBox.innerHTML = basePalette.map(function(x){
      return '<button class="sw" style="--sw:'+x.hex+'" title="'+x.hex+'" aria-label="'+x.hex+'" data-hex="'+x.hex+'"></button>';
    }).join('');
    swBox.addEventListener('click', function(e){
      var btn = e.target.closest('.sw'); if(!btn) return;
      var h = btn.getAttribute('data-hex');
      inputColor.value = h; inputHex.value = h; // 仅写入，不立刻执行
    });
    var moods = document.getElementById('cbx-moods');
    moods.innerHTML = moodMap.map(function(m){
      return '<button type="button" class="chip" data-hex="'+m.hex+'" title="'+m.name+'"><span class="dot" style="background:'+m.hex+'"></span>'+m.name+'</button>';
    }).join('');
    moods.addEventListener('click', function(e){
      var b = e.target.closest('.chip'); if(!b) return;
      addColor(b.getAttribute('data-hex'));
    });

    refreshPresets();

    // 控件：输入与 eyedropper
    var inputColor = document.getElementById('cbx-color');
    var inputHex = document.getElementById('cbx-hex');
    var btnAdd = document.getElementById('cbx-add');
    var btnDrop = document.getElementById('cbx-dropper');
    var rangeTol = document.getElementById('cbx-tol');
    var tolVal = document.getElementById('cbx-tol-val');

        // 逻辑/排序/L 事件
    var selLogic = document.getElementById('cbx-logic');
    var selRank = document.getElementById('cbx-rank');
    var rangeLP = document.getElementById('cbx-lp');
    var lpVal = document.getElementById('cbx-lp-val');

    if (selLogic) selLogic.addEventListener('change', function(){ state.logic = selLogic.value; searchAndRender(); syncURL(); saveRecent(); notifyColorResults(); });
    if (selRank) selRank.addEventListener('change', function(){ state.rank = selRank.value; searchAndRender(); syncURL(); saveRecent(); notifyColorResults(); });
    if (rangeLP) rangeLP.addEventListener('input', function(){ state.lpen = parseFloat(rangeLP.value)||0; lpVal.textContent = String(state.lpen); searchAndRender(); syncURL(); saveRecent(); notifyColorResults(); });
btnAdd.addEventListener('click', function(){ addColor(normalizeHex(inputHex.value || inputColor.value)); });
    inputHex.addEventListener('keydown', function(e){ if(e.key==='Enter'){ addColor(normalizeHex(inputHex.value)); }});
    // 用户修改 HEX 后（粘贴或输入后失焦），自动加入
    inputHex.addEventListener('change', function(){ inputHex.value = normalizeHex(inputHex.value); /* 等待确认 */ });
    inputColor.addEventListener('input', function(){ inputHex.value = normalizeHex(inputColor.value); });
    inputColor.addEventListener('change', function(){ inputHex.value = normalizeHex(inputColor.value); /* 等待确认 */ });
    rangeTol.addEventListener('input', function(){ state.tol = parseInt(rangeTol.value,10)||12; tolVal.textContent = state.tol; searchAndRender(); syncURL(); saveRecent(); notifyColorResults(); });

    btnDrop.addEventListener('pointerup', function(e){
      try{ e.preventDefault(); e.stopPropagation(); }catch(_){}
      // 延后一帧启动，避免触发本次点击被当成取色点
      requestAnimationFrame(function(){
        try{
          if ('EyeDropper' in window){
            var ed = new window.EyeDropper();
            ed.open().then(function(res){
              try{
                inputColor.value = res.sRGBHex;
                inputHex.value = normalizeHex(res.sRGBHex);
                addColor(normalizeHex(inputColor.value));
              }catch(__){}
            }).catch(function(){
              // 用户取消或不支持则无操作
            });
            return;
          }
        }catch(__){}
        try{
          if (inputColor && typeof inputColor.showPicker === 'function'){
            inputColor.showPicker(); // 选择完成后由 inputColor 'change' 事件自动加入
            return;
          }
        }catch(__){}
        // 最后兜底：聚焦到 HEX 输入
        try{ inputHex && inputHex.focus && inputHex.focus(); }catch(__){}
      });
    }););

    // 和声按钮
    Array.prototype.forEach.call(section.querySelectorAll('.harm'), function(b){
      b.addEventListener('click', function(){
        var k = b.getAttribute('data-k');
        if (state.harmony === k){ state.harmony = null; } else { state.harmony = k; }
        updateHarmonyButtons(); applyHarmony(); searchAndRender(); notifyColorResults(); syncURL();
      });
    });

    // 选择 chips 渲染
    function renderSelected(){
      var box = document.getElementById('cbx-selected');
      var chips = state.colors.map(function(h){
        return '<span class="chip" data-hex="'+h+'"><span class="dot" style="background:'+h+'"></span>'+h+'<button class="rm" title="移除" aria-label="移除">×</button></span>';
      }).join('');
      box.innerHTML = chips || '<span class="tip">尚未选择颜色。</span>';
      box.addEventListener('click', function(e){
        var rm = e.target.closest('.rm'); if(!rm) return;
        var chip = rm.closest('.chip'); var h = chip.getAttribute('data-hex');
        state.colors = state.colors.filter(function(x){ return x!==h; });
        chip.remove(); searchAndRender(); syncURL();
      notifyColorResults(); });
      // 移动端同步
      document.getElementById('cbx-sticky-chips').innerHTML = box.innerHTML;
      var rems = document.getElementById('cbx-sticky-chips').querySelectorAll('.rm');
      rems.forEach(function(x){ x.addEventListener('click', function(){ /* 同步上面已经处理 */ }); });
    }

    // 预设
    function refreshPresets(){
      var box = document.getElementById('cbx-presets');
      if (!state.presets.length){ box.innerHTML = '<span class="tip">暂无预设。设置好颜色后点击“存为预设”。</span>'; return; }
      box.innerHTML = state.presets.map(function(p, idx){
        var dots = p.colors.slice(0,5).map(function(h){ return '<span class="dot" style="display:inline-block;width:12px;height:12px;border-radius:50%;background:'+h+';border:1px solid rgba(0,0,0,.2)"></span>'; }).join('');
        return '<button type="button" class="chip" data-idx="'+idx+'" title="'+(p.name||('预设'+(idx+1)))+'">'+dots+' '+(p.name||('预设'+(idx+1)))+'</button>';
      }).join('');
      box.addEventListener('click', function(e){
        var b = e.target.closest('.chip'); if(!b) return;
        var i = parseInt(b.getAttribute('data-idx'),10);
        var preset = state.presets[i]; if(!preset) return;
        state.colors = uniq(preset.colors.map(normalizeHex)).slice(0,10);
        renderSelected(); searchAndRender(); syncURL();
      });
    }
    document.getElementById('cbx-save').addEventListener('click', function(){
      if (!state.colors.length) return;
      var name = prompt('预设名称（可留空）：') || '';
      state.presets.unshift({ name:name, colors: state.colors.slice(0,10) });
      state.presets = state.presets.slice(0,12);
      saveJSON('CBX_PRESETS', state.presets);
      refreshPresets();
    });
    document.getElementById('cbx-clear').addEventListener('click', function(){ state.colors = []; renderSelected(); searchAndRender(); syncURL(); });
    document.getElementById('cbx-sticky-clear').addEventListener('click', function(){ state.colors = []; renderSelected(); searchAndRender(); syncURL(); });
    document.getElementById('cbx-sticky-backtotop').addEventListener('click', function(){ window.scrollTo({ top:0, behavior:'smooth' }); });

    // 键盘：Cmd/Ctrl+K 聚焦 HEX 输入
    document.addEventListener('keydown', function(e){
      if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase()==='k')){
        e.preventDefault(); inputHex.focus(); inputHex.select();
      }
    });

    // 色板构造
    function buildBasePalette(rows, cols, includeGray){
      var shades = [];
      for (var r=0; r<rows; r++){
        for (var c=0; c<cols; c++){
          var l = 16 + c * (68/(cols-1)); // 16..84
          var h = (c*(360/cols))%360, s = 84 - r*(40/Math.max(1,(rows-1)));
          var rgb = hslToRgb(h, Math.max(24, Math.min(95, s)), Math.max(12, Math.min(88, l)));
          shades.push({ hex: rgbToHex(rgb.r, rgb.g, rgb.b) });
        }
      }
      if (includeGray){
        for (var g=0; g<cols; g++){
          var v = Math.round(8 + g * (92/(cols-1))); var rgb2 = hslToRgb(0, 0, v);
          shades.push({ hex: rgbToHex(rgb2.r, rgb2.g, rgb2.b) });
        }
      }
      var uniqs = uniq(shades.map(function(s){ return s.hex.toUpperCase(); }));
      return uniqs.map(function(h){ return {hex:h}; });
    }

    // 选择逻辑与存储
    function normalizeHex(h){
      if (!h) return null;
      h = h.toString().trim().replace(/^#?/,'#').toUpperCase();
      if (!/^#[0-9A-F]{6}$/.test(h)) return null;
      return h;
    }
    function uniq(arr){ var s={}; return arr.filter(function(x){ if(!x) return false; if(s[x]) return false; s[x]=1; return true; }); }
    function addColor(h){
      h = normalizeHex(h); if(!h) return;
      if (!state.colors.includes(h)){
        state.colors.push(h);
        state.colors = state.colors.slice(0,10);
        inputColor.value = h; inputHex.value = h;
        renderSelected(); applyHarmony(); searchAndRender(); notifyColorResults(); syncURL(); saveRecent();
      notifyColorResults(); }
    }
    function applyHarmony(){
      if (!state.harmony || !state.colors.length) return;
      var base = state.colors[0];
      var hsl = hexToHsl(base);
      var add = [];
      if (state.harmony === 'analogous'){
        [ -30, 30 ].forEach(function(shift){ var h=(hsl.h+shift+360)%360; var rgb=hslToRgb(h, hsl.s, hsl.l); add.push(rgbToHex(rgb.r,rgb.g,rgb.b)); });
      } else if (state.harmony === 'triad'){
        [ 120, 240 ].forEach(function(shift){ var h=(hsl.h+shift+360)%360; var rgb=hslToRgb(h, hsl.s, hsl.l); add.push(rgbToHex(rgb.r,rgb.g,rgb.b)); });
      } else if (state.harmony === 'complement'){
        var h=(hsl.h+180)%360; var rgb=hslToRgb(h, hsl.s, hsl.l); add.push(rgbToHex(rgb.r,rgb.g,rgb.b));
      }
      state.colors = uniq([].concat(state.colors, add)).slice(0,10);
      renderSelected();
    }
    function updateHarmonyButtons(){
      Array.prototype.forEach.call(section.querySelectorAll('.harm'), function(b){
        var k = b.getAttribute('data-k');
        b.classList.toggle('active', state.harmony === k);
      });
    }
    function syncURL(){
      try {
        var u = new url(location.href);
        if (state.colors.length) u.searchParams.set('colors', state.colors.join(',')); else u.searchParams.delete('colors');
        u.searchParams.set('tol', String(state.tol)); u.searchParams.set('logic', state.logic); u.searchParams.set('rank', state.rank); u.searchParams.set('lp', String(state.lpen));
        if (state.harmony) u.searchParams.set('harmony', state.harmony); else u.searchParams.delete('harmony');
        history.replaceState(null, '', u.toString());
      } catch(e){}
    }
    function loadJSON(key, def){ try{ return JSON.parse(localStorage.getItem(key)||'') || def; }catch(e){ return def; } }
    function saveJSON(key, val){ try{ localStorage.setItem(key, JSON.stringify(val)); }catch(e){} }
    function saveRecent(){ if (!state.colors.length) return; state.recent = uniq([state.colors[0]].concat(state.recent)).slice(0, 10); saveJSON('CBX_RECENT', state.recent); }

    // 字段兼容
    function pick(obj, keys, d){ for (var i=0;i<keys.length;i++){ var k=keys[i]; if (obj && obj[k]!=null && obj[k]!=='' ) return obj[k]; } return d; }
    function toArray(x){ if(Array.isArray(x)) return x; if(x==null) return []; return [x]; }
    function fmtDuration(v){
      if (typeof v === 'string' && /^\d{1,2}:\d{2}$/.test(v)) return v;
      var sec = 0;
      if (typeof v === 'number') sec = v>1000? Math.round(v/1000) : Math.round(v);
      if (typeof v === 'string' && /^\d+$/.test(v)) sec = parseInt(v,10);
      var m = Math.floor(sec/60); var s = sec%60;
      return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');
    }
    function ensurePreviewurl(t, fallback){
      var src = pick(t, ['preview_url','preview','url','audio','src'], null);
      if (!src) src = fallback || 'assets/demo.wav';
      return src;
    }
    function extractTags(t){
      var out = [];
      function add(x){ if(x && out.length<3) out.push(String(x)); }
      var arr = pick(t, ['tags','keywords'], null);
      if (Array.isArray(arr)) arr.forEach(add);
      [pick(t,['genre','style'],null), pick(t,['instrument','instruments'],null), pick(t,['scene','mood','emotion'],null)].forEach(add);
      return out.filter(Boolean).slice(0,3);
    }
    function licenseLabel(t){ return pick(t,['license_label','license','usage'], '商用·全球'); }

    // 播放控制
    function togglePlay(id, src, btn){
      if (!src) return;
      if (currentPlayId === id){
        if (!audio.paused){ audio.pause(); } else { audio.play().catch(function(){}); }
      } else {
        currentPlayId = id;
        audio.src = src;
        audio.play().catch(function(){});
      }
      updatePlayButtons();
    }
    function updatePlayButtons(){
      var list = document.querySelectorAll('.cbx .play-btn');
      list.forEach(function(b){
        var id = b.getAttribute('data-id');
        var isCurrent = (id === String(currentPlayId));
        b.classList.toggle('playing', isCurrent && !audio.paused && audio.src);
        b.setAttribute('aria-pressed', String(isCurrent && !audio.paused));
        b.innerHTML = (isCurrent && !audio.paused) ? '❚❚' : '▶';
      });
    }
    audio.addEventListener('play', updatePlayButtons);
    audio.addEventListener('pause', updatePlayButtons);
    audio.addEventListener('ended', function(){ currentPlayId = null; updatePlayButtons(); });

    // 搜索：与后端对接（多色；结果合并去重，按最小ΔE排序）
    function queryByColor(hex, tol){
return new Promise(function(resolve){
  try {
    var key = (hex+'|'+tol).toUpperCase();
    if (RESULT_CACHE[key]) { resolve(RESULT_CACHE[key].slice()); return; }
    if (LS_CACHE[key]) { RESULT_CACHE[key] = LS_CACHE[key]; resolve(LS_CACHE[key].slice()); return; }
    var u = new url(location.href);
    var api = u.origin + '/api/tracks';
    var q = new URLSearchParams({ color: hex.replace('#',''), tol: String(tol), mode: 'lab' });
    fetch(api + '?' + q.toString(), { credentials:'same-origin' })
      .then(function(res){ if(!res.ok) throw new Error('HTTP '+res.status); return res.json(); })
      .then(function(data){
        var arr = Array.isArray(data) ? data.map(function(x){ return {
          id: x.id, title: x.title, artist: x.artist,
          color: x.color || x.color_hex || hex,
          deltaE: x.deltaE,
          bpm: x.bpm || x.tempo,
          duration: x.duration || x.duration_ms || x.duration_s,
          tags: x.tags || x.keywords || [x.genre, x.instrument, x.mood].filter(Boolean),
          preview: ensurePreviewUrl(x, 'assets/demo.wav'),
          license_label: x.license_label || x.license
        }; }) : [];
        RESULT_CACHE[key] = arr; LS_CACHE[key] = arr; saveCache(LS_CACHE);
        resolve(arr.slice());
      }).catch(function(){
        // fallback：DEMO + local data/tracks.sample.json（若存在）
        var L1 = getLab(hex), d = deltaE2000(L1);
        var res = DEMO.map(function(t){ return Object.assign({}, t, { deltaE: d(getLab(t.color)) }); })
                       .filter(function(x){ return x.deltaE <= tol; })
                       .sort(function(a,b){ return a.deltaE-b.deltaE; });
        RESULT_CACHE[key] = res; LS_CACHE[key] = res; saveCache(LS_CACHE);
        resolve(res.slice());
      });
  } catch(e){
    var L1 = getLab(hex), d = deltaE2000(L1);
    var res = DEMO.map(function(t){ return Object.assign({}, t, { deltaE: d(getLab(t.color)) }); })
                   .filter(function(x){ return x.deltaE <= tol; })
                   .sort(function(a,b){ return a.deltaE-b.deltaE; });
    RESULT_CACHE[key] = res; LS_CACHE[key] = res; saveCache(LS_CACHE);
    resolve(res.slice());
  }
});

}

    // 批量查询并合并
    function searchAndRender(){
      var listBox = document.getElementById('cbx-list');
      var summary = document.getElementById('cbx-summary');
      listBox.innerHTML = '<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>';
      var colors = state.colors.length ? state.colors : ['#6EF3FF'];
      var tol = state.tol;

      // 拉取每个颜色的结果
      Promise.all(colors.map(function(h){ return queryByColor(h, tol).then(function(arr){ return {hex:h, items:arr}; }); }))
        .then(function(all){
          // 合并并计算多色评分
          var byId = Object.create(null);
          var colorLabs = colors.map(getLab);
          all.forEach(function(bucket, idx){
            var qhex = bucket.hex.toUpperCase();
            var qLab = colorLabs[idx];
            var d = deltaE2000(qLab);
            bucket.items.forEach(function(t){
              var id = String(t.id || (t.title+'|'+(t.artist||'')));
              var item = byId[id] || { id:id, title:t.title, artist:t.artist, bpm:t.bpm, duration:t.duration, preview:t.preview, license_label:t.license_label, tags: toArray(t.tags), per: {}, _seen:0 };
              var thex = (t.color || qhex).toUpperCase();
              var de = (typeof t.deltaE === 'number') ? t.deltaE : d(getLab(thex));
              item.per[qhex] = Math.min(de, 999);
              item.color = thex;
              // 基础字段累积
              if (t.bpm!=null) item.bpm = item.bpm || t.bpm;
              if (t.duration!=null) item.duration = item.duration || t.duration;
              if (t.preview) item.preview = item.preview || t.preview;
              if (t.license_label) item.license_label = item.license_label || t.license_label;
              if ((!item.tags || !item.tags.length) && t.tags) item.tags = toArray(t.tags);
              byId[id] = item;
            });
          });

          var list = Object.values(byId);

          // 逻辑过滤 & 评分
          function aggregateScore(per){
            var vals = colors.map(function(h){ return per[h.toUpperCase()]; });
            var missing = vals.filter(function(v){ return v==null; }).length;
            var present = vals.filter(function(v){ return v!=null; });
            if (state.logic === 'all' && missing>0) return null;
            // Soft: 缺失按 tol 计入
            var use = vals.map(function(v){ return v==null ? tol : v; });
            var score;
            switch (state.rank){
              case 'min': score = Math.min.apply(null, use); break;
              case 'avg': score = use.reduce(function(a,b){return a+b;},0)/use.length; break;
              case 'max': score = Math.max.apply(null, use); break;
              case 'weighted':
              default:
                // 加权：对更接近的匹配给予更高权重（1/(1+de))，聚合为加权平均
                var weights = use.map(function(v){ return 1/(1+v); });
                var sumw = weights.reduce(function(a,b){return a+b;},0) || 1;
                var wavg = use.reduce(function(a,v,i){ return a + v*(weights[i]/sumw); }, 0);
                score = wavg;
                break;
            }
            return score;
          }

          list = list.map(function(it){
            var baseScore = aggregateScore(it.per);
            if (baseScore==null) return null;
            // L 亮度惩罚（与每个命中颜色的 L 差平均）
            var lpen = +state.lpen || 0;
            if (lpen>0){
              var ldiffs = [];
              colors.forEach(function(h){
                var v = it.per[h.toUpperCase()];
                if (v!=null){
                  var qL = getLab(h).l;
                  var tL = getLab(it.color).l;
                  ldiffs.push(Math.abs(qL - tL));
                }
              });
              var lmean = ldiffs.length? (ldiffs.reduce(function(a,b){return a+b;},0)/ldiffs.length) : 0;
              baseScore += lpen * (lmean/10); // 亮度差 10 约等于 1 分惩罚（可调）
            }
            it.deltaE = baseScore;
            return it;
          }).filter(Boolean);

          // 排序
          list.sort(function(a,b){ return a.deltaE - b.deltaE; });

          // 渲染（虚拟列表：分批）
          listBox.innerHTML = '';
          listBox.insertAdjacentHTML('beforeend',
            '<div class="track-row head muted">'+
              '<div></div>'+
              '<div>标题 / 艺术家</div>'+
              '<div>标签</div>'+
              '<div>BPM</div>'+
              '<div>时长</div>'+
              '<div>授权</div>'+
              '<div style="text-align:right">操作</div>'+
            '</div>');

          var page = 0;
          function renderPage(){
            var start = page * state.pageSize;
            var end = Math.min(list.length, start + state.pageSize);
            for (var i=start; i<end; i++){
              var t = list[i];
              var id = String(t.id || ('t-'+i));
              var title = escapeHtml(t.title || '未命名');
              var artist = escapeHtml(t.artist || 'Ruotong Studio');
              var tags = (t.tags||[]).filter(Boolean).slice(0,3);
              var bpm = t.bpm!=null ? String(t.bpm) : '—';
              var dur = fmtDuration(t.duration||0);
              var badge = escapeHtml(licenseLabel(t));
              var src = ensurePreviewUrl(t, 'assets/demo.wav');
              var colorDot = '<span class="dot" style="--sw:' + escapeAttr(t.color||'#cccccc') + '"></span>';
              var hint = colors.map(function(h){ var v=t.per[h.toUpperCase()]; return h.toUpperCase()+': ' + (v!=null? v.toFixed(1):'—'); }).join(' · ');

              listBox.insertAdjacentHTML('beforeend',
                '<div class="track-row" data-id="'+escapeAttr(id)+'">'+
                  '<div>'+colorDot+'</div>'+
                  '<div><div class="title">'+title+'</div><div class="artist muted">'+artist+'</div></div>'+
                  '<div>'+ (tags.map(function(tg){return '<span class="tag">'+escapeHtml(tg)+'</span>';}).join('')) +'</div>'+
                  '<div>'+bpm+'</div>'+
                  '<div>'+dur+'</div>'+
                  '<div><span class="badge">'+badge+'</span></div>'+
                  '<div class="ops" title="'+escapeAttr(hint)+'">'+
                    '<button class="play" data-src="'+escapeAttr(src)+'">试听</button>'+
                    '<button class="like" data-id="'+escapeAttr(id)+'">收藏</button>'+
                  '</div>'+
                '</div>');
            }
            if (end < list.length){
              page++;
              requestIdleCallback ? requestIdleCallback(renderPage) : setTimeout(renderPage, 0);
            }
            // 更新摘要
            summary.textContent = '已选：' + colors.join(', ') + ' · 逻辑：' + state.logic + ' · 排序：' + state.rank + ' · 结果：' + list.length + ' 条';
          }
          renderPage();
        });

}

// --- RT Enhancement: notify color-results section when color changes ---
function notifyColorResults(){
  try{
    var hex = (state && state.colors && state.colors.length) ? state.colors[0] : null;
    if(!hex) return;
    var tol = state && typeof state.tol === 'number' ? state.tol : 18;
    document.dispatchEvent(new CustomEvent('rt:color:search', { detail: { hex: hex, tol: tol } }));
  }catch(e){}
}
// --- End Enhancement ---

    function escapeHtml(s){ return String(s||'').replace(/[&<>'"]/g, function(c){ return ({'&':'&amp;','<':'&lt;','>':'&gt;',\"'\":'&#39;','\"':'&quot;'})[c]; }); }
    function escapeAttr(s){ return String(s||'').replace(/"/g, '&quot;'); }

    // 初始：根据 URL / state 渲染
    if (!state.colors.length) state.colors = ['#6EF3FF'];
    renderSelected();
    updateHarmonyButtons();
    searchAndRender();
    syncURL();
  }
})();
